Ana içeriğe geç
  1. 100 Günde SwiftUI Notları/

39.Gün - SwiftUI: ScrollView ve NavigationLink

Bugün üzerinde çalışmamız gereken beş konu var: Container, Relative Frames, ScrollView , NavigationLink ve daha fazlası.

SwiftUI Görüntüleri Mevcut Alana Göre Yeniden Boyutlandırma #

SwiftUI’de bir Image view oluşturduğumuzda, otomatik olarak içeriğinin boyutlarına göre kendini boyutlandıracaktır. Yani, eğer görüntü 1000x500 ise, Image view’de 1000x500 olacaktır. Bu bazen istediğimiz bir şeydir, ancak çoğunlukla görüntüyü daha düşük bir boyutta göstermek isteriz. İşte bu başlıkta bunu inceleyeceğiz ve relative frame kullanarak görüntüyü kullanıcının ekranına nasıl sığdıracağımızı öğreneceğiz.

İlk olarak, projeye bir görsel ekleyin, ekrandan daha geniş olduğu sürece ne olduğu önemli değil. Ben benimkine “Example” adını verdim. Aşağıdaki görseli projede kullanacağım ve orijinal boyutu 1200x695 px

Original Image

Şimdi bu görüntüyü ekrana çizelim;

struct ContentView: View {
    var body: some View {
        Image("Example")
    }
}

İpucu : Bunun gibi sabit resim adları kullandığınızda, Xcode bunların tümü için string yerine kullanabileceğimiz sabit adlar oluşturur. Bu durumda, bu Image(.example) yazmak anlamına gelir, bu da bir string kullanmaktan çok daha güvenli.

Original Image Preview

Uygulama önizlemesinde, bu görselin mevcut alan için çok büyük olduğunu görebilirsiniz. Görüntüler diğer view’larla aynı frame() modifier’ına sahiptir, bu nedenle bu şekilde küçültmeyi deneyebiliriz.

Image(.example)
    .frame(width: 300, height: 300)

Ancak bu işe yaramayacaktır, resmimiz hala tam boyutunda görünecektir. Nedenini öğrenmek için, Xcode’un önizleme modunu “Live” ‘dan “Selectable”’a değiştirin.

Xcode preview mode change

Önemli : Bu işlem önizlemenizin canlı olarak çalışmasını durdurur, dolayısıyla bunun yerine Live seçeneğini belirleyene kadar view ile etkileşime giremezsiniz.

Selectable mod etkinken, önizleme penceresine yakından bakın, görüntümüzün tam boyutta olduğunu göreceksiniz, ancak şimdi ortada 300x300 boyutunda bir kutu var. Image view’ın çerçevesi (frame) doğru şekilde ayarlanmıştır, ancak görüntünün içeriği hala orijinal boyutunda gösterilmektedir.

SwiftUI Image Frame

Resmi bu şekilde değiştirelim;

Image(.example)
    .frame(width: 300, height: 300)
    .clipped()

SwiftUI Image Frame Clipped

Şimdi her şeyi daha net göreceksiniz: Image view gerçekten de 300x300, ancak istediğimiz aslında bu değil.

Resim içeriğinin de yeniden boyutlandırılmasını istiyorsak, resizable() modifier’ını şu şekilde kullanmamız gerekir;

Image(.example)
    .resizable()
    .frame(width: 300, height: 300)

SwiftUI Image Frame Resizable

Bu biraz daha iyi. Evet, görüntü şimdi doğru şekilde yeniden boyutlandırılıyor, ancak muhtemelen sıkışmış görünüyor. Görüntü kare değildi, bu sebeple kare şekline yeniden boyutlandırıldığı için bozuk görünüyor.

Bunu düzeltmek için, görüntünün kendisini orantılı olarak yeniden boyutlandırmamız gerekir. Bu da scaledToFit() ve scaledToFill() modifier’ları kullanılarak yapılabilir.

scaledToFit() görüntünün bazı kısımlarını boş bırakmak anlamına gelse bile tüm görüntünün container içine sığacağı anlamına gelir.

scaledToFill() görüntünün bir kısmının container dışında kalması anlamına gelse bile, view’da boş kısım kalmayacağı anlamına gelir.

Farkı kendiniz görmek için ikisini de deneyin. İşte .fit modu uygulanmış;

Image(.example)
    .resizable()
    .scaledToFit()
    .frame(width: 300, height: 300)

SwiftUI Image scaledToFit

Ve işte scaledToFill() ;

Image(.example)
    .resizable()
    .scaledToFill()
    .frame(width: 300, height: 300)

SwiftUI Image scaledToFill

Sabit boyutlu görüntüler istiyorsak tüm bunlar harika çalışır, ancak çoğu zaman ekranın bir veya her iki boyutunda daha fazlasını doldurmak için otomatik olarak ölçeklenen görüntüler isteriz. Yani, 300’lük bir genişliği sabit kodlamak yerine, gerçekten söylemek istediğimiz şey “bu görüntünün ekranın genişliğinin %80’ini doldurmasını sağla” ‘dır.

SwiftUI, belirli bir çerçeveyi zorlamak yerine, tam olarak istediğimiz sonucu elde etmemizi sağlayan özel bir containerRelativeFrame() modifier’ına sahiptir. “Container” kısmı tüm ekran olabilir, ancak aynı zamanda ekranın sadece bu view’ın yakın parent’inin kapladığı kısmı da olabilir, belki de görüntümüz diğer view’lar ile birlikte bir VStack içinde gösteriliyordur.

Örneğin ekranın %80 genişliğinde bir görüntü oluşturabiliriz.

Image(.example)
    .resizable()
    .scaledToFit()
    .containerRelativeFrame(.horizontal) { size, axis in
        size * 0.8
    }

SwiftUI Image Container Relative Frame

Bu kodu parçalara ayıralım;

  1. Bu görüntüye, parent’inin yatay boyutuna göre bir frame(çerçeve) vermek istediğimizi söylüyoruz. Dikey bir boyut belirtmiyoruz.
  2. SwiftUI daha sonra bize bir size ve bir axis verilen bir closure çalıştırır. Bizim için axis .horizontal olacak çünkü kullandığımız eksen bu, ancak bu daha çok relative yatay ve dikey boyutlar oluşturduğumuzda önemlidir. size değeri, tüm ekranımız olan container’ın boyutu olacaktır.
  3. Bu eksen için istediğimiz boyutu döndürmemiz gerekiyor, bu yüzden container’ın genişliğinin %80’ini gönderiyoruz.

Yine burada bir yükseklik belirtmemize gerek yok. Bunun nedeni SwiftUI’ye yüksekliği otomatik olarak bulabilmesi için yeterince bilgi vermiş olamamızdır. Orijinal genişliği, hedef genişliğimizi ve içerik modumuzu bildiği için görüntünün hedef yüksekliğinin hedef genişlikle nasıl orantılı olacağını anlar.

SwiftUI ScrollView ile Verileri Gösterme #

List ve Form’un kayan veri tabloları oluşturmamıza izin verdiğini gördünüz. Ancak kendi oluşturduğumuz veriler için ScrollView’e ihtiyacımız var.

Scroll View yatay, dikey veya her iki yönde de kaydırılabilir ve sistemin bunların yanında kaydırma çubuklarını da gösterip göstermeyeceğini de kontrol edebilirsiniz. View’ları scroll view içine yerleştirdiğimizde, kullanıcıların bir kenardan diğerine kaydırabilmesi için bu içeriğin boyutunu otomatik olarak hesaplanır.

Örnek olarak, aşağıdaki gibi 100 text view’den oluşan kayan bir liste oluşturabiliriz.

ScrollView {
    VStack(spacing: 10) {
        ForEach(0..<100) {
            Text("Item \($0)")
                .font(.title)
        }
    }
}

Simülatörde çalıştırdığımızda, scroll view’i serbestçe kaydırabileceğimizi göreceğiz, aşağı doğru kaydırdığımızda ScrollView’in safe area’yı tıpkı List ve Form gibi ele aldığını göreceksiniz. Yani içerikler Home Indicator’ün altına gider.

Ayrıca doğrudan merkeze dokunmanın biraz can sıkıcı olduğunu fark edebilirsiniz, tüm alanın kaydırılabilir olması daha yaygındır. Bu davranışı elde etmek için, VStack’in daha fazla yer kaplamasını sağlamalıyız.

ScrollView {
    VStack(spacing: 10) {
        ForEach(0..<100) {
            Text("Item \($0)")
                .font(.title)
        }
    }
    .frame(maxWidth: .infinity)
}

Artık ekranda herhangi bir yere dokunup sürükleyebiliriz, bu da daha kullanıcı dostudur.

Tüm bunlar gerçekten basit görünüyor, ancak bilmemiz gereken önemli bir nokta var. Bir scroll view’e view eklediğimizde bunlar hemen oluşturulur. Bunu göstermek için, normal bir text view’i etrafına aşağıdaki gibi basit bir wrapper ekleyebiliriz

struct CustomText: View {
    let text: String

    var body: some View {
        Text(text)
    }

    init(_ text: String) {
        print("Creating a new CustomText")
        self.text = text
    }
}

Şimdi bunu ForEach içinde kullanabiliriz:

ForEach(0..<100) {
    CustomText("Item \($0)")
        .font(.title)
}

Sonuç aynı görünecek, ancak şimdi uygulamayı çalıştırdığınızda Xcode’un loglarında yüzlerce kez “Creating a new CustomText” yazdığını göreceksiniz. SwiftUI bu logları göstermek için aşağı kaydırmanızı beklemeyecek, hemen oluşturcaktır.

Bunun olmasını önlemek istiyorsak, VStack ve HStack için LazyVStack ve LazyHStack alternatifleri vardır. Bunlar normal Stack’ler ile tamamen aynı şekilde kullanılabilir, ancak içeriklerini isteğe bağlı olarak yüklerler. Yani gerçekten gösterilinceye kadar view oluşturmazlar ve böylece kullanılan sistem kaynaklarının miktarını en aza indirirler.

Yani, bu durumda VStack’imizi aşağıdaki gibi LazyVStack ile değiştirebiliriz.

LazyVStack(spacing: 10) {
    ForEach(0..<100) {
        CustomText("Item \($0)")
            .font(.title)
    }
}
.frame(maxWidth: .infinity)

Normal ve Lazy stack’leri kullanma kodu aynı olsa da, önemli bir fark vardır. Lazy Stack’ler her zaman mümkün olan en fazla alanı kaplamaya çalışırken, normal Stack’ler yalnızca ihtiyaçları kadar alanı kaplarla. Bu kasıtlıdır, çünkü daha fazla alan isteyen yeni bir view yüklendiğinde lazy stack’lerin boyutlarını ayarlamak zorunda kalmasını önler.

Son bir şey, ScrollView’i oluştururken parametre olarak .horizontal değerini geçerek yatay ScrollView oluşturabilirsiniz. Bunu yaptıktan sonra, yatay bir Stack oluşturduğunuza emin olun.

ScrollView(.horizontal) {
    LazyHStack(spacing: 10) {
        ForEach(0..<100) {
            CustomText("Item \($0)")
                .font(.title)
        }
    }
}

NavigationStack view’lerin en üstünde bir navigation bar gösterir ama aynı zamanda başka bir şey daha yapar; view’ları bir yığın haline getirmemizi sağlar. Aslında bu, iOS navigasyonunun en temel biçimidir, bu durumu Ayarlarda Wi-Fi veya Genel’e dokunduğunuzda görebilirsiniz.

Bu view stack sistemi daha önce kullandığımız sheet’den çok farklıdır. Evet, her ikisi de bir tür yeni view gösteriyor, ancak sunulma biçimlerinde kullanıcıların bu view’lar hakkındaki düşüncelerini etkileyen bir fark var.

Kendi görebilmeniz için bazı kodlara bakarak başlayalım, bir navigation stack içinde basit bir metin görünümünü şu şekilde gösterebiliriz;

struct ContentView: View {
    var body: some View {
        NavigationStack {
            Text("Tap Me")
                .navigationTitle("SwiftUI")
        }
    }
}

SwiftUI NavigationStack

Bu text view sadece statik bir metindir, kendisine bağlı herhangi bir eylemi olan bir buton değildir. Bunu, kullanıcı üzerine dokunduğunda ona yeni bir view sunacak şekilde yapacağız. Bunu NavigationLink kullanarak yapacağız. NavigationLink’e bir hedef ve dokunulabilecek bir şey verin gerisini o halledecektir.

Bunu denemek için view’ı şu şekilde değiştirelim;

NavigationStack {
    NavigationLink("Tap Me") {
        Text("Detail View")
    }
    .navigationTitle("SwiftUI")
}

SwiftUI NavigationLink

“Tap Me” artık bir buton gibi görünüyor ve ona dokunduğumuzda sağdan “Detail View” yazan yeni bir view’ın kaydığını göreceksiniz. Daha da iyisi, “SwiftUI” başlığının aşağı doğru hareket ederek bir geri düğmesine dönüştüğünü ve geri dönmek için buna dokunabileceğimizi veya sol kenardan kaydırabileceğinizi göreceksiniz.

Label olarak basit bir text view’dan başka bir şey istiyorsanız, NavigationLink’e birlikte closure kullanabilirsiniz. Örneğin, birkaç text view’dan oluşan bir label yapabiliriz.

NavigationStack {
    NavigationLink {
        Text("Detail View")
    } label: {
        VStack {
            Text("This is the label")
            Text("So is this")
            Image(systemName: "face.smiling")
        }
        .font(.largeTitle)
    }
}

SwiftUI NavigationLink with custom label

Dolayısıyla, hem sheet() hem de NavigationLink mevcut view’dan yeni bir view göstermemize izin verir, ancak bunu yapma şekilleri farklıdır ve bunları dikkatlice seçmelisiniz ;

  • NavigationLink bir konuyu derinlemesine araştırıyormuşsunuz gibi kullanıcının seçimiyle ilgili ayrıntıları göstermek içindir.
  • sheet() ayarlar veya bilgi girişi sağlayan ekranlar gibi ilgisiz içerikleri göstermek içindir.

NavigationLink’i en sık gördüğünüz yer bir listedir ve SwiftUI burada oldukça harika bir şey yapar.

Kodumuzu şu şekilde değiştirmeyi deneyelim;

NavigationStack {
    List(0..<100) { row in
        NavigationLink("Row \(row)") {
            Text("Detail \(row)")
        }
    }
    .navigationTitle("SwiftUI")
}

SwiftUI NavigationStack with List

Codable Data ile Çalışma #

Codable protokolü ile bir türün tek bir örneğini veya bu örneklerin bir array veya dictionary’sini çözüyorsanız işler yolunda gider. Ancak bu projede biraz daha karmaşık JSON kod çözeceğiz, farklı veri türleri kullanan bir array içinde array olacak.

Bu tür hiyerarşik verilerin kodunu çözmek istiyorsanız, en iyi yol sahip olduğunuz her seviye için ayrı türler oluşturmaktır. Veriler, istediğiniz hiyerarşi ile eşleştiği sürece, Codable her şeyin kodunu çözebilir.

Bunu göstermek için, bu butonu content view’e yerleştirin.

Button("Decode JSON") {
    let input = """
    {
        "name": "Taylor Swift",
        "address": {
            "street": "555, Taylor Swift Avenue",
            "city": "Nashville"
        }
    }
    """

    // more code to come
}

Yukarıdaki kod bir JSON string oluşturur. Yukarıdaki JSON ile eşleşen struct’ları oluşturalım;

struct User: Codable {
    let name: String
    let address: Address
}

struct Address: Codable {
    let street: String
    let city: String
}

Şimdi en iyi kısma gelelim: JSON stringi Data tipine (Codable’ın çalıştığı tip) dönüştürülebilir ve ardından bunu bir User instance’a dönüştürebiliriz.

let data = Data(input.utf8)
let decoder = JSONDecoder()
if let user = try? decoder.decode(User.self, from: data) {
    print(user.address.street)
}

Bu programı çalıştırır ve butona dokunursanız adresin yazdırıldığını görürsünüz.

Codable’ın geçeceği seviye sayısında bir sınır yoktur, önemli olan tek şey tanımladığınız struct’ların JSON string ile eşleşmesidir.

View’leri Scrolling Grid’e Ekleme #

SwiftUI’nin List view’ı, kayan veri satırlarını göstermenin harika bir yoludur, ancak bazen veri sütunları da isteyebiliriz. Örneğin, daha büyük ekranlarda daha fazla veri gösterecek şekilde uyarlanabilen ızgara görünümüne ihtiyaç duyabiliriz.

SwiftUI’de bu iki view ile gerçekleştirilebilir; Yatay verileri göstermek için LazyHGrid ve dikey verileri göstermek için LazyVGrid . Tıpkı lazy stack’lerde olduğu gibi, adın “lazy” kısmı, SwiftUI’nin içerdiği view’ların yüklenmesini ihtiyaç duydukları ana kadar otomatik olarak geciktireceği için ordadır, bu da çok fazla sistem kaynağını kullanmadan daha fazla veri görüntüleyebileceğimiz anlamına gelir.

Grid view oluşturmak iki adımda yapılır. İlk olarak, istediğimiz satırları veya sütunları tanımlamamız gerekir (Hangi grid türünü istediğimize bağlı olarak yalnızca ikisinden birini tanımlarız)

Örneğin, dikey olarak kayan bir grid’imiz varsa, view’a bu özelliği ekleyerek verilerimizin tam olarak 80 pt genişliğinde üç sütuna yerleştirilmesini istediğimizi söyleyebiliriz.

let layout = [
    GridItem(.fixed(80)),
    GridItem(.fixed(80)),
    GridItem(.fixed(80))
]

Layout’u tamamladıktan sonra, grid’i istediğimiz kadar öğeyle birlikte bir ScrollView içine yerleştirmeliyiz. Grid içinde oluşturduğumuz her öğeye otomatik olarak bir sütun atanır.

Örneğin, üç sütunlu grid’in içinde 1000 öğeyi şu şekilde oluşturabiliriz.

ScrollView {
    LazyVGrid(columns: layout) {
        ForEach(0..<1000) {
            Text("Item \($0)")
        }
    }
}

SwiftUI GridView

Bu bazı durumlarda işe yarar, ancak grid’in en iyi yanı çeşitli ekran boyutlarında çalışabilmelidir. Bu aşağıdaki gibi adaptive boyutlar kullanılarak farklı bir sütun layout ile yapılabilir.

let layout = [
    GridItem(.adaptive(minimum: 80)),
]

Bu, SwfitUI’ye en az 80 pt genişliğinde oldukları sürece mümkün olduğunca çok sütun sığdırmaktan mutlu olduğumuzu söyler. Daha fazla kontrol için maksimum aralık da belirleyebiliriz.

let layout = [
    GridItem(.adaptive(minimum: 80, maximum: 120)),
]

SwiftUI GridView Adaptive Layout

Bu layout ile ekran alanını maksimum düzeyde kullanabiliyoruz.

Yatay grid’lerde de işlem neredeyse aynıdır. Sadece ScrollView’ın yatay olarak çalışmasını sağlamamız ve ardından columns yerine rows kullanarak LazyHGrid oluşturmamız gerekir.

ScrollView(.horizontal) {
    LazyHGrid(rows: layout) {
        ForEach(0..<1000) {
            Text("Item \($0)")
        }
    }
}

Bu yazıyı İngilizce olarak da okuyabilirsiniz.
You can also read this article in English.

Bu yazı, SwiftUI Day 39 adresinde bulunan yazılardan kendim için aldığım notları içermektedir. Orjinal dersi takip etmek için lütfen bağlantıya tıklayın.